/*
 * Copyright 2010-2012 Susanta Tewari. <freecode4susant@users.sourceforge.net>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

package genomemap.worker;

import genomemap.data.DataSource;

import gui.genomemap.inputModel.AnnealIModel;

import javautil.collections.ArrayUtil;
import javautil.io.IOUtil;
import javautil.io.LogUtil;

/**
 * Searches for the Best order by minimizing the objective function
 * (defined by the interface  <code>Annealable</code>). it invokes
 * <code>annealable.compute()</code> to get the value of the objective
 * function at each order.
 * Note: if the intention is to find the maximum of the objective function,
 * invocation of <code>annealable.compute()</code> should yield negative
 * of the function value. For example, in GLikeliHood compute returns the negative
 * of the log-likelihood value (getLHood returns a potive log-likelihood). Since we are searching
 * for the order with MAX Likelihood, the converged value is set with the negative of
 * <code>bestValue</code> in method <code>run()</code> with the code
 * <code>setConvValue(-bestValue)</code>;
 * @author Susanta Tewari
 * @version 1.0
 * @created 08-Nov-2007 2:39:12 PM
 *
 */
public class SimulatedAnnealing extends AnnealMachine {

    /**
     *  TOTAL # OF ITERATIONS TO BE TRIED IN THE ENTIRE ANNEALING MACHINE.
     *  AFTER WHICH THE MACHINE IS STOPPED IF NOT CONVERGED ALREADY.
     */
    private int MASTER_ITER_COUNT = 50000;

    /**
     * TOTAL # OF ITERATIONS TO BE TRIED UNDER A TEMPERATURE,
     * AFTER WHICH THE TEMPERATURE IS REDUCED BY FORCE IF NOT SKIPPED ALREADY
     */
    private int TEMP_ITER_COUNT = 1000;

    /**
     * STARTING TEMPERATURE
     */
    private double START_TEMP = 10.0;

    /**
     * TEMPERATURE FRICTION
     */
    private double TEMP_FRICTION = 0.25;

    /**
     *  MAXIMUM UNBEATEN COUNT
     */
    private int MAX_UNBEATEN_COUNT = 500;

    /**
     * MAXIMUM BEATEN COUNT
     */
    private int MAX_BEATEN_COUNT = 50;

    /**
     * verbose LOGGING
     */
    private boolean verbose = false;

    /**
     * debug LOGGING
     */
    private boolean debug = false;

    /**
     * percent: % of liklelihood improvement considered
     */
    private double percent = 0.001;

    /** Field description */
    private Annealable annealable = null;

    /**
     * Constructs ...
     */
    private SimulatedAnnealing() {}

    /**
     * Note that parameters related to an annealable object contained in the input
     * model will be applied to the annelable from the calling program.
     * @param inputModel
     */
    public SimulatedAnnealing(AnnealIModel inputModel) {

        setMASTER_ITER_COUNT(inputModel.getMASTER_ITER_COUNT());
        setTEMP_ITER_COUNT(inputModel.getTEMP_ITER_COUNT());
        setSTART_TEMP(inputModel.getSTART_TEMP());
        setTEMP_FRICTION(inputModel.getTEMP_FRICTION());
        setMAX_UNBEATEN_COUNT(inputModel.getMAX_UNBEATEN_COUNT());
        setMAX_BEATEN_COUNT(inputModel.getMAX_BEATEN_COUNT());
        setPercent(inputModel.getPercent());
    }

    public static void main(String[] args) {

        /**
         * JLikeliHood jHood = new JLikeliHood();
         * jHood.setGIterationLimit(1);
         * jHood.setVerbose(false);
         * jHood.setJData(DataSource.getJDataSource(7, new int[]{1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,32,33,34,35,36,37,38,39,40,41,42,43,44,45,46,47,48,49,50,51,52,53,54,55,56,57,58,59,60,61,62,63,64,65,66,67,68,69,70,71,72,73,74,75,76,77,78,79,80,81,82,83,84,85,86,87,88,89,90,91,92,93,94,95,96,97,98,99,100,101,102,103,104,105,106,107,108,109,110,111,112,113,114,115,116,117,118,119,120,121,122,123,124,125,126,127,128,129,130,131,132,133,134,135,136,137,138,139,140,141,142,143,144,145,146,147,148,149,150,151,152,153,154,155,156,157,158,159,160,161,162,163,164,165}));
         *
         * SimulatedAnnealing annealMachine = new SimulatedAnnealing();
         * annealMachine.setverbose(true);
         *
         * annealMachine.run(jHood);
         */
        PLikeliHood pHood = new PLikeliHood();

        pHood.setPData(DataSource.getPDataSource(7, new int[] {

            1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24,
            25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35, 36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46,
            47, 48, 49, 50, 51, 52, 53, 54, 55, 56, 57, 58, 59, 60, 61, 62, 63, 64, 65, 66, 67, 68,
            69, 70, 71, 72, 73, 74, 75, 76, 77, 78, 79, 80, 81, 82, 83, 84, 85, 86, 87, 88, 89, 90,
            91, 92, 93, 94, 95, 96, 97, 98, 99, 100, 101, 102, 103, 104, 105, 106, 107, 108, 109,
            110, 111, 112, 113, 114, 115, 116, 117, 118, 119, 120, 121, 122, 123, 124, 125, 126,
            127, 128, 129, 130, 131, 132, 133, 134, 135, 136, 137, 138, 139, 140, 141, 142, 143,
            144, 145, 146, 147, 148, 149, 150, 151, 152, 153, 154, 155, 156, 157, 158, 159, 160,
            161, 162, 163, 164, 165

        }));

        SimulatedAnnealing annealMachine = new SimulatedAnnealing();

        annealMachine.setverbose(true);
        annealMachine.setAnnealable(pHood);
        annealMachine.run();
    }

    /**
     *
     * @param annealable
     */
    @Override
    public void run() {

        setStatusInfo("Simulated Annealing has Started");
        setProgress(10);

        /* COUNTER FOR MASTER_ITER_COUNT */
        int master_iter_counter = 0;

        /* COUNTER FOR MASTER_COUNT */
        int temp_iter_counter = 0;

        /* COUNTER FOR THE MAXIMUM UNBEATEN COUNT */
        int max_unbeaten_counter = 0;

        /* COUNTER FOR THE MAXIMUM BEATEN COUNT */
        int max_beaten_counter = 0;

        /* ORDERS */
        int[] oldOrder   = null;
        int[] newOrder   = null;
        int[] bestOrder  = null;
        double oldValue  = 0;
        double newValue  = 0;
        double bestValue = 0;

        /* HOUSEKEEPING VARIABLES */
        double delta = 0.0;
        double t     = START_TEMP;

        /* INITIALIZE oldOrder */
        oldOrder = annealable.getOrder().clone();

        /* COMPUTE oldValue */
        oldValue = annealable.initialValue();

        /* THE BEST ORDER bestOrder AND THE BEST VALUE bestValue ARE INITIALIZED TO THE OLD ORDER oldOrder AND THE OLD VALUE oldValue RESPECTIVELY */
        bestOrder = oldOrder.clone();
        bestValue = oldValue;

        /* CREATE A NEW ORDER: newOrder : & is used to get the range 1 through nprobe to be compatible with the legacy code */
        newOrder = getNewOrder(oldOrder.clone());

        /* START THE PROCESS: ENTER INTO THE ANNEALING LOOP */
        do {

            master_iter_counter++;

            do {

                temp_iter_counter++;

                /* Compute the newValue based on newOrder */
                annealable.setOrder(newOrder);

                newValue = annealable.compute();

                /* compare */
                delta = newValue - oldValue;

                /* Based on comparision Reverse the order */
                if (delta >= 0) {

                    /*
                     *  The beating is NOT SUCCESSFUL but it`s given a random chance.
                     * Only the newOrder and the
                     */
                    if (Math.random() < Math.exp(-delta / t)) {

                        oldValue = newValue;

                        /* Transformation is BASED ON newOrder */
                        oldOrder = newOrder.clone();
                        newOrder = getNewOrder(newOrder.clone());

                    } else {

                        /* Transformation is BASED ON oldOrder */
                        newOrder = getNewOrder(oldOrder.clone());
                    }
                } else {

                    /* AT LEAST LOCAL BEATING */


                    // if ((newValue - bestValue) < 0 && bestValue / newValue > (1 + percent)) {
                    if ((newValue - bestValue) < 0) {

                        /* THE BEATING IS GLOBAL:(AT LEAST 1% INCREASE IN LIKELIHOOD) */
                        /* GLOBAL PARAMETERS ARE ADJUSTED */
                        max_unbeaten_counter = 0;
                        bestValue            = newValue;
                        bestOrder            = newOrder.clone();
                    }

                    /* TREATMENT COMMON TO LOCAL AND GLOBAL BEATING */
                    max_beaten_counter++;

                    oldValue = newValue;
                    oldOrder = newOrder.clone();
                    newOrder = getNewOrder(newOrder.clone());
                }

                max_unbeaten_counter++;

                if ((max_beaten_counter >= MAX_BEATEN_COUNT)
                        || (max_unbeaten_counter >= MAX_UNBEATEN_COUNT)) {
                    break;
                }

                String longMsg = (MAX_UNBEATEN_COUNT - max_unbeaten_counter) + " steps left: "
                                 + "At Temperature " + t + " order "
                                 + IOUtil.toString(ArrayUtil.getIntegerArray(oldOrder), ",")
                                 + " with Likelihood = " + oldValue + " is competing order  "
                                 + IOUtil.toString(ArrayUtil.getIntegerArray(newOrder), ",")
                                 + " \n";
                String shortMsg = (MAX_UNBEATEN_COUNT - max_unbeaten_counter) + " steps left: "
                                  + "At Temperature " + t + " newValue is " + newValue
                                  + " and the bestValue is " + bestValue;

                if (debug) {
                    System.out.println(longMsg);
                }

                if (verbose) {
                    System.out.println(shortMsg);
                }

                setStatusInfo(shortMsg);
                setLog(longMsg);
                setProgress((int) ((double) max_unbeaten_counter / MAX_UNBEATEN_COUNT * 100));

            } while (temp_iter_counter <= TEMP_ITER_COUNT);

            if (max_unbeaten_counter >= MAX_UNBEATEN_COUNT) {

                setConverged(true);
                setConvValue(-bestValue);
                setConvOrder(bestOrder);
                setStatusInfo("The Simulated Annealing has Converged");
                setProgress(100);

                if (verbose || debug) {

                    System.out.println("\n");
                    System.out.println("THE SIMULATED ANNEALING HAS CONVERGED");
                    System.out.println("BEST ORDER IS CHOSEN BASED ON " + (percent * 100)
                                       + "% LIKELIHOOD IMPROVEMENTS");
                    System.out.println("THE CONVERGED ORDER HAS LIKELIHOOD: " + bestValue);
                    System.out.println("THE CONVERGED TEMPERATURE: " + t);
                    System.out.println("THE CONVERGED ORDER: " + t);
                    LogUtil.print(bestOrder);
                    System.out.println("\n");
                }

                break;
            }

            t                  = t * TEMP_FRICTION;
            max_beaten_counter = 0;
            temp_iter_counter  = 0;

        } while (master_iter_counter <= MASTER_ITER_COUNT);

        if (master_iter_counter >= MASTER_ITER_COUNT) {

            setConverged(false);
            setConvValue(bestValue);
            setConvOrder(bestOrder);
            setStatusInfo("The Simulated Annealing didn`t Converge");
            setProgress(100);

            if (verbose || debug) {

                System.out.println(
                    "THE SIMULATED ANNEALING HAS HAS USED UP THE TOTAL NUMBER OS ALLOWED ITERATIONS");
                System.out.println("BEST ORDER IS CHOSEN BASED ON " + (percent * 100)
                                   + "% LIKELIHOOD IMPROVEMENTS");
                System.out.println(" THE CONVERGED ORDER HAS LIKELIHOOD: " + bestValue);
                System.out.println(" THE CONVERGED TEMPERATURE: " + t);
                System.out.println(" THE CONVERGED ORDER : " + t);
                LogUtil.print(bestOrder);
                System.out.println();
            }
        }
    }

    /**
     * TOTAL # OF ITERATIONS TO BE TRIED IN THE ENTIRE ANNEALING MACHINE. AFTER WHICH
     * THE MACHINE IS STOPPED IF NOT CONVERGED ALREADY.
     */
    public int getMASTER_ITER_COUNT() {
        return MASTER_ITER_COUNT;
    }

    /**
     * MAXIMUM BEATEN COUNT
     */
    public int getMAX_BEATEN_COUNT() {
        return MAX_BEATEN_COUNT;
    }

    /**
     * MAXIMUM UNBEATEN COUNT
     */
    public int getMAX_UNBEATEN_COUNT() {
        return MAX_UNBEATEN_COUNT;
    }

    /**
     * percent: % of liklelihood improvement considered
     */
    public double getPercent() {
        return percent;
    }

    /**
     * STARTING TEMPERATURE
     */
    public double getSTART_TEMP() {
        return START_TEMP;
    }

    /**
     * TEMPERATURE FRICTION
     */
    public double getTEMP_FRICTION() {
        return TEMP_FRICTION;
    }

    /**
     * TOTAL # OF ITERATIONS TO BE TRIED UNDER A TEMPERATURE, AFTER WHICH THE
     * TEMPERATURE IS REDUCED BY FORCE IF NOT SKIPPED ALREADY
     */
    public int getTEMP_ITER_COUNT() {
        return TEMP_ITER_COUNT;
    }

    /**
     * debug LOGGING
     */
    public boolean isdebug() {
        return debug;
    }

    /**
     * verbose LOGGING
     */
    public boolean isverbose() {
        return verbose;
    }

    /**
     * debug LOGGING
     *
     * @param newVal
     */
    public void setdebug(boolean newVal) {
        debug = newVal;
    }

    /**
     * TOTAL # OF ITERATIONS TO BE TRIED IN THE ENTIRE ANNEALING MACHINE. AFTER WHICH
     * THE MACHINE IS STOPPED IF NOT CONVERGED ALREADY.
     *
     * @param newVal
     */
    void setMASTER_ITER_COUNT(int newVal) {
        MASTER_ITER_COUNT = newVal;
    }

    /**
     * MAXIMUM BEATEN COUNT
     *
     * @param newVal
     */
    void setMAX_BEATEN_COUNT(int newVal) {
        MAX_BEATEN_COUNT = newVal;
    }

    /**
     * MAXIMUM UNBEATEN COUNT
     *
     * @param newVal
     */
    void setMAX_UNBEATEN_COUNT(int newVal) {
        MAX_UNBEATEN_COUNT = newVal;
    }

    /**
     * percent: % of liklelihood improvement considered
     *
     * @param newVal
     */
    void setPercent(double newVal) {
        percent = newVal;
    }

    /**
     * STARTING TEMPERATURE
     *
     * @param newVal
     */
    void setSTART_TEMP(double newVal) {
        START_TEMP = newVal;
    }

    /**
     * TEMPERATURE FRICTION
     *
     * @param newVal
     */
    void setTEMP_FRICTION(double newVal) {
        TEMP_FRICTION = newVal;
    }

    /**
     * TOTAL # OF ITERATIONS TO BE TRIED UNDER A TEMPERATURE, AFTER WHICH THE
     * TEMPERATURE IS REDUCED BY FORCE IF NOT SKIPPED ALREADY
     *
     * @param newVal
     */
    void setTEMP_ITER_COUNT(int newVal) {
        TEMP_ITER_COUNT = newVal;
    }

    /**
     * verbose LOGGING
     *
     * @param newVal
     */
    void setverbose(boolean newVal) {
        verbose = newVal;
    }

    public Annealable getAnnealable() {
        return annealable;
    }

    public void setAnnealable(Annealable annealable) {
        this.annealable = annealable;
    }
}
